Classificação de acordes

Nossa proposta consiste em implementar uma ferramenta automática para identificação de tríades de formação de acordes, e consequentemente identificar os acordes que compõem gravações de instrumentos. Convencionalmente, tal identificação é feita através da escuta de trechos da gravação, por músicos treinados, que escrevem manualmente as cifras à medida que são identificadas.

A implementação consiste em uma análise do módulo do espectro de frequências da gravação em relação com o tempo, de modo a obter em intervalos de tempo fixos as notas predominantes e as tríades formadas por tais notas. As perguntas que motivam este trabalho são: qual grau de assertividade que uma análise nessas condições pode promover? Esse grau de assertividade auxiliaria um músico ou uma musicista a escrever cifras de determinada gravação?

Introdução

O espectro de frequências e fast Fourier transform

Em processamento digital de áudio, a análise no domínio da frequência, principalmente o de amplitude, pode trazer bastante informação sobre o sinal, mais do que a simples análise temporal.

In [25]:
Fs = 44100
Tmax = 2
A3 = 440.0
E3 = 329.63
t = np.linspace(0, Tmax, Tmax*Fs)
s = 0.7*np.sin(2*np.pi*A3*t) + 0.3*np.sin(2*np.pi*E3*t)
ipd.Audio(s, rate=Fs)
Out[25]:
In [26]:
plt.figure(figsize=(15,5))
plt.plot(t, s)
plt.xlabel('Tempo (s)')
plt.ylabel('Intensidade')
plt.xlim(0.0, 0.025)
Out[26]:
(0.0, 0.025)
In [27]:
S = fftpack.fft(s)
S = S/len(S)
f = np.linspace(-Fs/2, Fs/2, len(S))
Splot = np.abs(fftpack.fftshift(S))
plt.figure(figsize=(15,5))
plt.plot(f, Splot)
plt.xlabel('Frequência (Hz)')
plt.ylabel('Intensidade')
plt.xlim(300, 500)
Out[27]:
(300, 500)

Análise tempo-frequência e short-time Fourier transform

É possível dividir as amostras de um sinal de áudio em janelas de tempo, de modo que possamos determinar o espectro periódico do sinal.

In [28]:
tchirp = np.linspace(0, 3, 3*Fs)
s = signal.chirp(tchirp, 200.0, 3, 1000.0, 'logarithmic')
ipd.Audio(s, rate=Fs)
Out[28]:
In [29]:
f, t, Stf = signal.stft(s, fs=Fs, nperseg=2048)
plt.figure(figsize=(15,5))
Stfplot = np.abs(Stf)
plt.pcolormesh(t, f, Stfplot)
plt.ylabel('Frequência (Hz)')
plt.xlabel('Tempo (s)')
plt.ylim(0, 1000)
Out[29]:
(0, 1000)

Note que, por conta da FFT aplicada em uma janela de aproximadamente 50ms, há uma imprecisão na medida de frequência vista por uma linha espessa no gráfico. Ao aumentar o tamanho dessa janela, obtemos maior precisão da frequência, mas perdemos informação temporal.

In [30]:
tsig = np.linspace(0, 1, Fs)
tsil = np.linspace(0, 0.5, int(0.5*Fs))
# 2 segundos de 200Hz, silencio e 2 segundos de 300Hz
s1 = np.sin(2*np.pi*200.0*tsig)
silent = 1e-7*tsil
s2 = np.sin(2*np.pi*300.0*tsig)
s = np.concatenate((s1, silent, s2))

f, t, Stf = signal.stft(s, fs=Fs, nperseg=2048)
plt.figure(figsize=(15,5))
Stfplot = np.abs(Stf)
plt.pcolormesh(t, f, Stfplot)
plt.ylabel('Frequência (Hz)')
plt.xlabel('Tempo (s)')
plt.ylim(0, 600)
Out[30]:
(0, 600)

Frequências e escala cromática

A escala cromática contém 12 notas com intervalos de semitons. Como duas oitavas consecutivas possuem a relação de dobro/metade da frequência, define-se a relação de frequência entre semitons:

$$ {ST}_{n} = 2^{1/12} \cdot {ST}_{n-1} = 2^{n/12} \cdot {ST}_{0} $$

Sendo $${ST}_{0} = 55Hz$$ o Lá Zero (A0).

Podemos entender essa relação simétrica entre semitons desenhada por um círculo.

Figura 1 - Escala cromática vista como um círculo (Autor: David Eppstein)

Em termos de frequência, podemos associar as notas a pontos de uma curva helicoidal, cuja projeção destes pontos obtém o círculo da figura 1, e a distância entre pontos da mesma projeção (o passo da hélice) cresce o dobro em relação à anterior.

Figura 2 - Escala cromática vista como uma helicoidal

Vale ressaltar que muitos sistemas de afinações não usam esta escala simétrica de semitons.

Metodologia

Obtenção de notas do espectro de frequência

Passos seguidos:

  1. Relação de espectro (módulo ao quadrado) e tempo obtida pela STFT
  2. Para cada janela de tempo
    1. Determinar a frequência de centro pela equação $${ST}_{n} = 2^{n/12} \cdot {ST}_{0}$$
    2. Determinar os limites inferior e superior como $${ST}_{n} \pm 2.8\%$$ Baseado na distância média entre duas frequências de semitons
    3. Somar em uma lista de 12 notas os valores das amostras entre os limites
      1. n = 0, 12, 24, ... em A
      2. n = 1, 13, 25, ... em A#
      3. n = 2, 15, 26, ... em B
      4. ...
    4. Parar quando chegar em 20 kHz (ou metade da frequência de amostragem, segundo teorema de Nyquist-Shannon)
    5. Normalizar a lista dividindo pela soma
  3. O retorno é uma tabela cujas linhas são as notas e as colunas são as janelas de tempo em que se aplicou a STFT
In [32]:
Fs, data = wavfile.read('../wav/ChromaticScaleUp.wav')
ipd.Audio(data, rate=Fs)
Out[32]:
In [33]:
f, t, Stf = signal.stft(data, fs=Fs, nperseg=2048)
plt.figure(figsize=(15,5))
Stfplot = np.abs(Stf)
plt.pcolormesh(t, f, Stfplot)
plt.ylabel('Frequência (Hz)')
plt.xlabel('Tempo (s)')
plt.ylim(0, 2000)
Out[33]:
(0, 2000)
In [34]:
scale, t, Ch = chromagram_stft(data, rate=Fs)

plt.figure(figsize=(15,5))
chromaplot(t, scale, Ch)
plt.xlabel('Tempo (s)')
Out[34]:
Text(0.5, 0, 'Tempo (s)')

Obtenção de um acorde

Separamos as notas em tríades e decidimos o acorde maior, menor, diminuto ou aumentado

In [35]:
sample = 'C-A'

Fs, data = wavfile.read('../wav/{}.wav'.format(sample), 44100)
ipd.Audio(data, rate=Fs)
Out[35]:
In [36]:
f, t, Stf = signal.stft(data, fs=Fs, nperseg=2048)
plt.figure(figsize=(15,5))
Stfplot = np.abs(Stf)
plt.pcolormesh(t, f, Stfplot)
plt.ylabel('Frequência (Hz)')
plt.xlabel('Tempo (s)')
plt.ylim(20, 2000)
Out[36]:
(20, 2000)
In [37]:
scale, t, Ch = chromagram_stft(data, rate=Fs)

plt.figure(figsize=(15,5))
chromaplot(t, scale, Ch)
plt.xlabel('Tempo (s)')
Out[37]:
Text(0.5, 0, 'Tempo (s)')
In [38]:
note_groups = classifier.get_note_list(data, rate=Fs)

# Plotting results
plottable_groups = np.zeros(shape=(12, len(note_groups)))
scale = [s for s in scale]
for i in range(len(note_groups)):
    for note in note_groups[i]:
        plottable_groups[scale.index(note), i] = 1
        
plt.figure(figsize=(15,5))
chromaplot(t, scale, plottable_groups)
plt.xlabel('Tempo (s)')
Out[38]:
Text(0.5, 0, 'Tempo (s)')
In [40]:
plt.figure(figsize=(1,1))
chromaplot([0,1], ['Major', 'Minor', 'Diminished', 'Augmented', ''], [[4], [3], [2], [1], [0]])

plt.figure(figsize=(15,5))
chromaplot(np.linspace(t[0], t[-1], num=int(len(t)/window)), scale, plottable_chords)
plt.xlabel('Tempo (s)')
Out[40]:
Text(0.5, 0, 'Tempo (s)')
In [41]:
from precision_checker import print_precisions

print_precisions(sample)
The exact precision is 55.56%
The near precision is 55.56%

Testando com gravações

  1. Flamenco com um violão
In [42]:
sample = 'Reggae1'
Fs, data = wavfile.read('../wav/{}.wav'.format(sample), 44100)
scale, t, Ch = chromagram_stft(data, rate=Fs)
ipd.Audio(data, rate=Fs)
Out[42]:
In [43]:
note_groups = classifier.get_note_list(data, rate=Fs)

# Plotting results
plottable_groups = np.zeros(shape=(12, len(note_groups)))
scale = [s for s in scale]
for i in range(len(note_groups)):
    for note in note_groups[i]:
        plottable_groups[scale.index(note), i] = 1
        
plt.figure(figsize=(15,5))
chromaplot(t, scale, plottable_groups)
plt.xlabel('Tempo (s)')
Out[43]:
Text(0.5, 0, 'Tempo (s)')
In [45]:
plt.figure(figsize=(1,1))
chromaplot([0,1], ['Major', 'Minor', 'Diminished', 'Augmented', ''], [[4], [3], [2], [1], [0]])

plt.figure(figsize=(15,5))
chromaplot(np.linspace(t[0], t[-1], num=int(len(t)/window)), scale, plottable_chords)
plt.xlabel('Tempo (s)')
Out[45]:
Text(0.5, 0, 'Tempo (s)')
In [46]:
print_precisions(sample)
The exact precision is 49.32%
The near precision is 53.88%
  1. Rock (trilha de karaokê)
In [47]:
sample = 'Rock1'
Fs, data = wavfile.read('../wav/{}.wav'.format(sample), 44100)
scale, t, Ch = chromagram_stft(data, rate=Fs)
ipd.Audio(data, rate=Fs)
Out[47]:
In [48]:
note_groups = classifier.get_note_list(data, rate=Fs)

# Plotting results
plottable_groups = np.zeros(shape=(12, len(note_groups)))
scale = [s for s in scale]
for i in range(len(note_groups)):
    for note in note_groups[i]:
        plottable_groups[scale.index(note), i] = 1
        
plt.figure(figsize=(15,5))
chromaplot(t, scale, plottable_groups)
plt.xlabel('Tempo (s)')
Out[48]:
Text(0.5, 0, 'Tempo (s)')
In [50]:
plt.figure(figsize=(1,1))
chromaplot([0,1], ['Major', 'Minor', 'Diminished', 'Augmented', ''], [[4], [3], [2], [1], [0]])

plt.figure(figsize=(15,5))
chromaplot(np.linspace(t[0], t[-1], num=int(len(t)/window)), scale, plottable_chords)
plt.xlabel('Tempo (s)')
Out[50]:
Text(0.5, 0, 'Tempo (s)')
In [51]:
print_precisions(sample)
The exact precision is 30.36%
The near precision is 31.55%